﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;

namespace Gw2MumbleLinkServer
{
    class HttpStream : Stream
    {
        /* This code has been copied from Gw2ExtendedTradingPost, with minor modifications to remove uneeded functionality */

        protected const int HTTP_STREAM_TYPE_HEADER_LENGTH = 3;
        protected const int HEADER_BUFFER_LENGTH = 512;
        protected const int MAXIMUM_HEADER_LENGTH = 1024 * 16;

        public enum StreamType { None, Unknown, Normal, Secure, SSL3, TLS1 };

        protected StreamType type;
        protected TcpClient client;
        protected HttpHeader header;

        protected Stream inner;
        protected byte[] bufferedData;      //data to be inserted into the stream when reading
        protected int bufferPosition;       //position in the buffer
        protected int bufferLength;         //length of the buffer

        protected long contentLength;
        protected long contentBytesRead;

        protected bool isContentChunked;
        protected int chunkLength;
        protected int chunkBytesRemaining;

        public HttpStream(Stream inner, TcpClient client)
        {
            this.inner = inner;
            this.type = StreamType.None;
            this.client = client;
        }

        public Stream BaseStream
        {
            get
            {
                return this.inner;
            }
            set
            {
                this.inner = value;
            }
        }

        public TcpClient Client
        {
            get
            {
                return this.client;
            }
        }

        private void ResetContentStream()
        {
            this.contentLength = 0;
            this.contentBytesRead = 0;
            this.isContentChunked = false;
            this.chunkLength = 0;
            this.chunkBytesRemaining = 0;
        }

        public HttpHeader ReadHeader()
        {
            ResetContentStream();

            byte[] buffer = new byte[HEADER_BUFFER_LENGTH];

            int bufferLength = HEADER_BUFFER_LENGTH;
            int bufferPosition = 0;
            int lastIndex = -1;
            int headerLength = -1;

            byte cr = (byte)'\r';
            byte lf = (byte)'\n';

            Stream inner = this.inner;

            do
            {
                if (bufferLength - bufferPosition < HEADER_BUFFER_LENGTH / 2)
                {
                    bufferLength += HEADER_BUFFER_LENGTH;
                    byte[] b = new byte[bufferLength];
                    Array.Copy(buffer, 0, b, 0, bufferPosition);
                    buffer = b;
                }

                int bytesRead;
                try
                {
                    bytesRead = inner.Read(buffer, bufferPosition, bufferLength - bufferPosition);
                }
                catch
                {
                    break; //Assuming no data was available, timeout
                }

                if (bytesRead > 0)
                {
                    int i = bufferPosition - 1;
                    while ((i = Array.IndexOf<byte>(buffer, lf, i + 1, bufferPosition + bytesRead - (i + 1))) >= 0)
                    {
                        //Searching for standard header terminator \r\n\r\n or \n\n
                        if (lastIndex != -1 && i - lastIndex <= 2)
                        {
                            byte prev = buffer[i - 1];
                            if (prev == cr || prev == lf)
                            {
                                headerLength = i + 1;
                                break;
                            }
                        }

                        lastIndex = i;
                    }

                    bufferPosition += bytesRead;
                }
                else
                    break;
            }
            while (headerLength == -1 && bufferLength < MAXIMUM_HEADER_LENGTH);

            if (headerLength > 0)
            {
                string header = Encoding.ASCII.GetString(buffer, 0, headerLength);

                if (headerLength < bufferPosition)
                    this.Insert(buffer, headerLength, bufferPosition - headerLength);

                this.header = HttpHeader.Create(header);

                ProcessHeader(this.header);

                return this.header;
            }
            else
            {
                this.Insert(buffer, 0, bufferPosition);

                throw new Exception("No headers available");
            }
        }

        public void Insert(byte[] data, int offset, int count)
        {
            if (count <= 0)
                return;

            byte[] buffer;
            int remaining = bufferLength - bufferPosition;

            if (remaining > 0)
            {
                bufferLength = remaining + count;
                buffer = new byte[bufferLength];

                Array.Copy(data, offset, buffer, 0, count);
                Array.Copy(bufferedData, bufferPosition, buffer, count, remaining);
            }
            else
            {
                bufferLength = count;
                buffer = new byte[bufferLength];

                Array.Copy(data, offset, buffer, 0, count);
            }

            bufferedData = buffer;
            bufferPosition = 0;
        }

        protected void ProcessHeader(HttpHeader header)
        {
            this.isContentChunked = header.IsChunked;
            this.contentLength = header.ContentLength;
        }

        private int _Read(byte[] buffer, int offset, int count)
        {
            int length;

            if (bufferPosition < bufferLength)
            {
                length = Math.Min(count, (int)(bufferLength - bufferPosition));
                Array.Copy(bufferedData, bufferPosition, buffer, offset, length);

                bufferPosition += length;

                if (bufferPosition == bufferLength)
                {
                    this.bufferLength = 0;
                    this.bufferPosition = 0;
                    this.bufferedData = null;
                }

                return length;
            }

            try
            {
                length = inner.Read(buffer, offset, count);
            }
            catch (Exception e)
            {
                return 0;
            }

            return length;
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            int bytesRead;

            if (isContentChunked)
            {
                if (chunkBytesRemaining == -1)
                    return 0;

                if (chunkBytesRemaining == 0)
                {
                    chunkLength = chunkBytesRemaining = ReadChunkHeader();
                    if (chunkLength == 0)
                    {
                        chunkBytesRemaining = -1;
                        return 0;
                    }
                }

                try
                {
                    bytesRead = this._Read(buffer, offset, Math.Min(chunkBytesRemaining, count));
                }
                catch (Exception e)
                {
                    throw e;
                }
                chunkBytesRemaining -= bytesRead;
            }
            else
            {
                if (contentLength != -1)
                {
                    count = Math.Min((int)(contentLength - contentBytesRead), count);
                    if (count == 0)
                        return 0;
                }

                try
                {
                    bytesRead = this._Read(buffer, offset, count);
                }
                catch (Exception e)
                {
                    throw e;
                }
            }

            contentBytesRead += bytesRead;
            return bytesRead;
        }

        private int ReadChunkHeader()
        {
            //example: \r\n[hex string]\r\n

            StringBuilder s = new StringBuilder(8);
            byte[] buffer = new byte[10];
            int bytesRead;
            bool found = false;

            while (!found && (bytesRead = this._Read(buffer, 0, buffer.Length)) > 0)
            {
                for (int i = 0; i < bytesRead; i++)
                {
                    char c = (char)buffer[i];
                    if (Char.IsLetterOrDigit(c))
                        s.Append(c);
                    else if (c == '\n' && s.Length > 0)
                    {
                        if (++i < bytesRead)
                            this.Insert(buffer, i, bytesRead - i);
                        found = true;
                        break;
                    }
                }
            }

            int length = 0;

            if (Int32.TryParse(s.ToString(), System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out length))
            {
                if (length == 0)
                {
                    //continue reading \r\n end of file
                    while ((bytesRead = this._Read(buffer, 0, 1)) > 0)
                    {
                        char c = (char)buffer[0];
                        if (c == '\n')
                            break;
                    }
                }
            }
            else
                throw new Exception("Unknown chunk header " + s.ToString());

            return length;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            try
            {
                inner.Write(buffer, offset, count);
            }
            catch (Exception e)
            {
                return;
            }
        }

        public void WriteHeader(string method, string location, Version version, WebHeaderCollection headers)
        {
            StringBuilder s = new StringBuilder(512);
            s.AppendLine(method + " " + location + " HTTP/" + version);

            WriteHeader(s, headers);
        }

        public void WriteHeader(Version version, HttpStatusCode statusCode, string statusDescription, WebHeaderCollection headers)
        {
            StringBuilder s = new StringBuilder(512);
            s.AppendLine("HTTP/" + version + " " + (int)statusCode + " " + statusDescription);

            WriteHeader(s, headers);
        }

        private void WriteHeader(StringBuilder s, WebHeaderCollection headers)
        {
            for (int i = 0, length = headers.Count; i < length; i++)
            {
                s.AppendLine(headers.GetKey(i) + ": " + headers[i]);
            }
            s.AppendLine();

            byte[] buffer = Encoding.ASCII.GetBytes(s.ToString());

            this.Write(buffer, 0, buffer.Length);
        }

        public override void Close()
        {
            this.inner.Close();
        }

        public override bool CanRead
        {
            get
            {
                return true;
            }
        }

        public override bool CanSeek
        {
            get
            {
                return false;
            }
        }

        public override bool CanWrite
        {
            get
            {
                return inner.CanWrite;
            }
        }

        public override void Flush()
        {
            inner.Flush();
        }

        public override long Length
        {
            get
            {
                throw new NotSupportedException();
            }
        }

        public override long Position
        {
            get
            {
                return inner.Position;
            }
            set
            {
                throw new NotSupportedException();
            }
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException();
        }

        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        #region HttpHeader

        public abstract class HttpHeader
        {
            protected HttpHeader(string command, WebHeaderCollection headers)
            {
                this.Headers = headers;

                ProcessCommand(command);
                ProcessHeaders(headers);
            }

            public static HttpHeader Create(string header)
            {
                StringReader reader = new StringReader(header);
                WebHeaderCollection headers = new WebHeaderCollection();

                try
                {
                    string line = reader.ReadLine();
                    while (line.Length == 0)
                        line = reader.ReadLine();

                    string command = line;

                    while ((line = reader.ReadLine()) != null && line.Length > 0)
                    {
                        int i = line.IndexOf(':');
                        if (i != -1)
                        {
                            string key = line.Substring(0, i);
                            string value = line.Substring(i + 1).Trim();
                            headers.Add(key, value);
                        }
                    }

                    if (command.ToLower().StartsWith("http/"))
                        return HttpResponseHeader.Create(command, headers);
                    else
                        return HttpRequestHeader.Create(command, headers);
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);
                    throw e;
                }
                finally
                {
                    reader.Dispose();
                }
            }

            protected abstract void ProcessCommand(string line);

            protected abstract void ProcessHeaders(WebHeaderCollection headers);

            public WebHeaderCollection Headers
            {
                get;
                protected set;
            }

            public int ContentLength
            {
                get
                {
                    if (this.Headers == null)
                        return -1;

                    int contentLength = -1;
                    string length;
                    if (this is HttpResponseHeader)
                        length = this.Headers[System.Net.HttpResponseHeader.ContentLength];
                    else
                        length = this.Headers[System.Net.HttpRequestHeader.ContentLength];
                    if (length != null)
                        Int32.TryParse(length, out contentLength);
                    return contentLength;
                }
            }

            public bool IsChunked
            {
                get
                {
                    if (this.Headers == null)
                        return false;

                    string transfer;
                    if (this is HttpResponseHeader)
                        transfer = this.Headers[System.Net.HttpResponseHeader.TransferEncoding];
                    else
                        transfer = this.Headers[System.Net.HttpRequestHeader.TransferEncoding];
                    return (transfer != null && transfer.ToLower() == "chunked");
                }
            }
        }

        public class HttpResponseHeader : HttpHeader
        {
            protected HttpResponseHeader(string command, WebHeaderCollection headers)
                : base(command, headers)
            { }

            public static HttpResponseHeader Create(string command, WebHeaderCollection headers)
            {
                return new HttpResponseHeader(command, headers);
            }

            protected override void ProcessCommand(string line)
            {
                //example: HTTP/1.1 200 OK
                string version, statusCode, statusDescription;
                int i, last;

                i = line.IndexOf(' ');
                version = line.Substring(0, i);
                last = i + 1;

                i = line.IndexOf(' ', i + 1);
                statusCode = line.Substring(last, i - last);

                statusDescription = line.Substring(i + 1);

                this.ProtocolVersion = new Version(version.Substring(version.IndexOf('/') + 1));
                this.StatusCode = (HttpStatusCode)Int32.Parse(statusCode);
                this.StatusDescription = statusDescription;
            }

            protected override void ProcessHeaders(WebHeaderCollection headers)
            {
            }

            public Version ProtocolVersion
            {
                get;
                protected set;
            }

            public HttpStatusCode StatusCode
            {
                get;
                protected set;
            }

            public string StatusDescription
            {
                get;
                protected set;
            }
        }

        public class HttpRequestHeader : HttpHeader
        {
            protected HttpRequestHeader(string command, WebHeaderCollection headers)
                : base(command, headers)
            { }

            public static HttpRequestHeader Create(string command, WebHeaderCollection headers)
            {
                return new HttpRequestHeader(command, headers);
            }

            protected override void ProcessCommand(string line)
            {
                //example: GET / HTTP/1.1
                string method, location, version;
                int i, last;

                i = line.IndexOf(' ');
                method = line.Substring(0, i);
                last = i + 1;

                i = line.LastIndexOf(' ');
                version = line.Substring(i + 1);

                location = line.Substring(last, i - last);

                this.ProtocolVersion = new Version(version.Substring(version.IndexOf('/') + 1));
                this.Method = method.ToUpper();
                this.Location = location;
            }

            protected override void ProcessHeaders(WebHeaderCollection headers)
            {
            }

            public string Method
            {
                get;
                private set;
            }

            public string Location
            {
                get;
                private set;
            }

            public Version ProtocolVersion
            {
                get;
                private set;
            }

            public string Host
            {
                get
                {
                    if (this.Headers == null)
                        return null;

                    return base.Headers[System.Net.HttpRequestHeader.Host];
                }
            }
        }

        #endregion HttpHeader
    }
}
